Hooks 核心原理
以 use 开头的函数被称为 Hook。
一、核心 Hooks 的底层逻辑
1. useState
React 维护一个记忆单元格链表(state list 类似于数组,在函数组件的对应的 Fiber 节点的 memoizedState 属性),每个 useState 对应一个单元格。首次渲染时初始化状态,后续渲染直接读取当前值。
- 首次调用
useState(initialValue)时,会初始化状态(若initialValue是函数则执行它获取初始值),并将状态存入链表节点,返回[当前状态, 更新函数] - 调用更新函数(如
setCount)时, React 会将新状态放入更新列队,并标记组件为“需要重新渲染” - 重新渲染时, React 会从
memoizedState的链表中按顺序读取最新的状态,保证每次渲染能获取到当前最新值
function useState(initial) {
const index = currentStateIndex++; // 获取当前的索引
const state = isMount ? initial : memoizedState[index]; // 读取状态
const setState = newVal => {
memoizedState[index] = newVal; // 更新链表节点
scheduleRender(); // 触发重新渲染
};
return [state, setState];
}
useState 的 setState 是替换(而非 class 组件的组合)
调用 setState 时, React 将新状态加入更新列队,触发重新渲染并生成新的状态快照。状态更新是异步的,但能保证同一事件循环中的多次调用按顺序合并处理。
2. useEffect
将副作用函数存入副作用列队(位于 Fiber 节点的updateQueue),通过依赖数组控制执行时机,在浏览器绘制完成后异步执行。
- 组件首次渲染后(DOM)更新后, React 会执行所有的
useEffect的副作用函数 - 重新渲染时, React 会浅比较当前依赖项数组与上一次:
- 若依赖项有变化:先执行上一次副作用的清理函数(若存在),再执行新的副作用函数
- 若依赖项无变化:仅在首次渲染后执行,组件卸载时执行清理函数
- 本质是利用“依赖项”变化触发副作用的更新,确保副作用与组件状态同步。
function useEffect(create, deps) {
// 获取旧依赖
const oldDeps = memoizedDeps[currentIndex];
// 浅比较依赖
let hasChange = !oldDeps || !shallowEqual(oldDeps, deps);
if (hasChange) {
// 清理旧副作用
cleanup(currentEffect);
// 储存新的 effect
currentEffect = {
create,
deps,
};,
// 安排清理
scheduleCleanup(() => cleanup(currentEffect));
}
}
React 会对依赖项进行浅比较,变化后重新执行:
- 无依赖数组:每次渲染后执行
- 空数组
[]:只在挂载时执行一次 - 有依赖数组
[dep1, dep2]:依赖项变化时执行 - 对象/数组:引用发生变化时触发执行
返回的函数会在下次执行前及组件卸载时调用,避免内存泄漏(如定时器、事件监听)
3. useContext
通过 Context 树查找,使用当前组件的 contextStackCursor 定位最近的 Provider 的值。
Context 系统包含 Provider (提供值)和 Consumer(消费值)。useContext 本质上是简化 Consumer 的使用:
- 当组件调用
useContext(Context)时,React 会从当前组件的 Fiber 节点开始,向上遍历 Fiber 树,寻找到最近的Context.Provider,并获取其value - 同时,组件会订阅该
Context的变化:当Provider的value变化时,所有订阅该Context的组件会被标记“需要重新渲染”,确保获取最新值
function useContext(Context) {
// 获取栈顶 Context
const contextItem = contextStackTop;
// 返回最新值
return contextItem && contextItem.Consumer === Context
? contextItem.value
: Context._currentValue;
}
当上下文值变化时,所有订阅该上下文的组件会重新渲染,无论嵌套层级多深。适用于全局配置(如主题、用户信息),但频繁更新可的上下文可能导致不必要的渲染。
4. useRef
返回一个可变对象 {current: value},储存在纤维节点(Fiber)上,跨渲染周期保留。
useRef 返回的 ref 对象 ({current: ...})会被储存到组件 Fiber 节点的 memoizedState 中,且在组件整个生命周期中保持唯一引用。
- 与
useState不同:useState的更新会触发重渲染,而useRef.current的修改仅改变容器内容,不影响组件渲染流程
function useRef(initial) {
const fiber = getCurrentFiber();
fiber.refs ||= {};
// 初始化储存
fiber.refs.myRef ||= { current: initial };
return fiber.refs.myRef;
}
修改 .current 的值不会触发重新渲染,是个储存可变值(如定时器 ID)。
二、核心规则
React 依赖 Hooks 调用的顺序来维护状态与副作用的映射关系。
因为 React 会为每一个组件创建的 Fiber 节点的 memoizedState 链表(state list),按调用顺序建立映射。
- ✅ 只能在函数组件/自定义 Hook 顶层调用
- ❌ 禁止在循环/条件/嵌套函数中使用
如果在条件、循环中调用 Hooks,会导致每次渲染时 Hooks 调用顺序或数量变化,React 无法匹配状态链表,引发状态混乱。
如果漏写依赖数组也会导致回调函数捕获到“旧值”,引发逻辑错误。
三 useEffect 依赖陷阱
1. 无依赖项无限循环
未将副作用中使用的外部变量加入依赖项
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
2. 依赖项冗余无限循环
在 useEffect 中更新依赖项导致无限循环的更新。
useEffect(() => {
setTime(new Date());
}, [time]);
3. 异步操作陷阱
由于异步获取的旧值。
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(count);
}, 1000);
return () => clearInterval(timer);
}, []);
避免可通过 useCallback 缓存函数或者将变量作为依赖项。
四、useState 的初始化延迟计算
useState 的初始值可以通过函数延迟,这在初始化计算成本较高(如解析大 JSON、复杂运算)时非常有用,避免重复计算。
useState 的第一个参数可以是值,也可以是返回初始值的函数。如果是函数,React 仅在组件首次渲染时调用一次,后续渲染会复用手机计算的值。
// 直接传递值(每次渲染都会检测,但不会重新计算)
const [state, setState] = useState(expensiveCalculation());
// 延迟计算:传递函数(仅首次渲染时执行)
const [lazyState, setLazyState] = useState(() => expensiveCalculation());
- 优势:避免每次组件渲染时都执行高开销的初值计算(如解析本地储存、网络请求结果预处理等)
- 注意:如果初始值依赖
props,需确保props变化时能更新状态。此时延迟初始化函数可能无法感知props变化(因为它仅执行一次),需结合useEffect或useMemo处理